#include "zen_macro.h"

#if defined(ZEN_OS_ANDROID)

#include "zen_utils.h"
#include "zen_utils_android.h"
#include "zen_exception.h"
#include "zen_utils_android.h"
#include "zen_log.h"
#include "zen_file.h"
#include "zen_utils_android.h"

static JavaVM *sJavaVM = nullptr;
static jint sJNIVersion = 0;

extern "C" JNIEXPORT void JNICALL
Java_zen_utils_Utils_initializeNative(JNIEnv *env, jclass clazz) {
    Zen::LogD("init env: %x, %s", env, __func__);

    if (sJavaVM) return;

    JavaVM *vm = nullptr;

    if (env->GetJavaVM(&vm) == JNI_OK) {
        auto version = env->GetVersion();
        sJavaVM = vm;
        sJNIVersion = version;
        Zen::LogD("set java vm. %s vm:%x, version:%d", __func__, vm, version);
    } else {
        Zen::LogE("java vm not set. %s", __func__);
    }
}

namespace Zen {
    using namespace std;

#define CLASS_NAME "zen/utils/Utils"

    class UtilsAndroid__ : public UtilsAndroid {
    public:
        JNIEnv *GetJNIEnv() override {
            auto vm = GetJavaVM();
            if (!vm) {
                Zen::LogE("java vm not setting. %s", __func__);
                return nullptr;
            }

            JNIEnv *env = nullptr;
            if (vm->GetEnv((void **) &env, GetJNIVersion()) == JNI_OK) {
                Zen::LogD("%s env: %d", __func__, env);
                return env;
            }
            Zen::LogE("java vm Get NULL env");
            return nullptr;
        }

        JavaVM *GetJavaVM() override {
            if (!sJavaVM) {
                Zen::LogE(
                        "call:%f error:javaVM is nil, you should call zen_utils_Utils_initializeNative first",
                        __func__);
            }
            return sJavaVM;
        }

        int GetJNIVersion() override {
            return (int) sJNIVersion;
        }

        /// callJava
        std::tuple<JNIEnv *, jclass, jmethodID> FindJavaMethod
                (cstring &cls, cstring &method, cstring &type) override {
            JNIEnv *env = GetJNIEnv();
            __zen_must_else(env, "null java JNIEnv");

            jclass jc = env->FindClass(cls.c_str());
            __zen_must_else(jc, "cannot find java class:" + cls);

            jmethodID jm = env->GetStaticMethodID(jc, method.c_str(), type.c_str());

            __zen_must_else(jm, "cannot find java method:" + method);

            return {env, jc, jm};
        }

        void CallJavaVoidMethod(cstring &cls, cstring &fun) override {
            auto j = FindJavaMethod(cls, fun, "()V");
            JNIEnv *env = std::Get<0>(j);
            env->CallStaticVoidMethod(std::Get<1>(j), std::Get<2>(j));
        }

        void CallJavaVoidMethod(cstring &cls, cstring &fun, cstring &arg0) override {
            auto j = FindJavaMethod(cls, fun, "(Ljava/lang/String;)V");
            JNIEnv *env = std::Get<0>(j);
            jstring js0 = env->NewStringUTF(arg0.c_str());
            env->CallStaticVoidMethod(std::Get<1>(j), std::Get<2>(j), js0);
        }

        bool CallJavaBoolMethod(cstring &cls, cstring &fun) override {

            auto j = FindJavaMethod(cls, fun, "()Z");
            JNIEnv *env = std::Get<0>(j);
            return JNI_FALSE != env->CallStaticBooleanMethod(std::Get<1>(j), std::Get<2>(j));
        }

        bool CallJavaBoolMethod(cstring &cls, cstring &fun, cstring &arg0) override {
            auto j = FindJavaMethod(cls, fun, "(Ljava/lang/String;)Z");
            JNIEnv *env = std::Get<0>(j);
            auto js0 = env->NewStringUTF(arg0.c_str());
            return JNI_FALSE != env->CallStaticObjectMethod(std::Get<1>(j), std::Get<2>(j), js0);
        }

        std::string CallJavaStringMethod(cstring &cls, cstring &fun) override {
            auto j = FindJavaMethod(cls, fun, "()Ljava/lang/String;");
            JNIEnv *env = std::Get<0>(j);
            auto res = env->CallStaticObjectMethod(std::Get<1>(j), std::Get<2>(j));
            if (!res) {
                LogE("%s::%s return null string.", cls.data(), fun.data());
                return "";
            }
            return env->GetStringUTFChars((jstring)res, 0);
        }

        std::string CallJavaStringMethod(cstring &cls, cstring &fun, cstring &arg0) override {
            auto j = FindJavaMethod(cls, fun, "(Ljava/lang/String;)Ljava/lang/String;");
            JNIEnv *env = std::Get<0>(j);
            auto js0 = env->NewStringUTF(arg0.c_str());
            auto res = env->CallStaticObjectMethod(std::Get<1>(j), std::Get<2>(j), js0);
            if (!res) {
                LogE("%s::%s return null string.", cls.data(), fun.data());
                return "";
            }
            return env->GetStringUTFChars((jstring)res, 0);
        }

        std::vector<uint8_t> CallJavaBytesMethod(cstring &cls, cstring &fun) override {
            auto j = FindJavaMethod(cls, fun, "()[B");
            JNIEnv *env = std::Get<0>(j);
            auto res = env->CallStaticObjectMethod(std::Get<1>(j), std::Get<2>(j));
            if (!res) {
                LogE("%s::%s return null object.", cls.data(), fun.data());
                return {};
            }
            auto len = env->GetArrayLength((jbyteArray)res);
            auto value = env->GetByteArrayElements((jbyteArray)res, 0);
            return std::vector<uint8_t>(value, value + len);
        }

        std::vector<uint8_t>
        CallJavaBytesMethod(cstring &cls, cstring &fun, cstring &arg0) override {
            auto j = FindJavaMethod(cls, fun, "(Ljava/lang/String;)[B");
            JNIEnv *env = std::Get<0>(j);
            auto js0 = env->NewStringUTF(arg0.c_str());
            auto res = env->CallStaticObjectMethod(std::Get<1>(j), std::Get<2>(j), js0);
            if (!res) {
                LogE("%s::%s return null object.", cls.data(), fun.data());
                return {};
            }
            auto len = env->GetArrayLength((jbyteArray)res);
            auto value = env->GetByteArrayElements((jbyteArray)res, 0);
            return std::vector<uint8_t>(value, value + len);
        }
    public:

        std::string GetFilesDir() override {
            static std::string path;
            if (path.size()) return path;

            path = CallJavaStringMethod(CLASS_NAME, "GetFilesDir");
            return path;
        }

        std::string GetCacheDir() override {
            static std::string path;
            if (path.size()) return path;
            path = CallJavaStringMethod(CLASS_NAME, "GetCacheDir");
            return path;
        }

        std::string GetExternalCacheDir() override {
            static std::string path;
            if (path.size()) return path;
            path = CallJavaStringMethod(CLASS_NAME, "GetExternalCacheDir");
            return path;
        }

        std::string GetDocumentPath(std::string const &path) override {
            if (path.empty()) return GetFilesDir();
            if (path[0] == '/') return path;
            return GetFilesDir() + "/" + path;
        }

        std::string GetAppID() override {
            static std::string app_id;
            if (app_id.size()) return app_id;

            app_id = CallJavaStringMethod(CLASS_NAME, "GetAppID");
            if(app_id.empty()) {
                LogE("cannot Get a valid package name");
            }
            return app_id;
        }

        std::string GetDeviceID() override {
            static std::string device_id;
            if (device_id.size()) return device_id;

            device_id = CallJavaStringMethod(CLASS_NAME, "GetDeviceID");
            if(device_id.empty()) {
                LogE("cannot Get a valid device id");
            }
            return device_id;
        }

        bool IsResourceExist(std::string const &path) override {

            return CallJavaBoolMethod(CLASS_NAME, "IsResourceExist", path);
        }

        std::vector<uint8_t> LoadResource(std::string const &path) override {
            return CallJavaBytesMethod(CLASS_NAME, "LoadResource", path);
        }

        bool IsDocumentExist(std::string const &path) override {
            return access(GetDocumentPath(path).c_str(), 0) == 0;
        }

        std::vector<uint8_t> LoadDocument(std::string const &path) override {
            auto doc = GetDocumentPath(path);
            LogD("%s path:%s", __func__, doc.data());
            return Zen::LoadFileToBuffer(doc);
        }

        bool SaveDocument(std::string const &path, void const *value, size_t size) override {
            auto doc = GetDocumentPath(path);
            LogD("%s path:%s %x %u", __func__, doc.data(), value, size);
            auto ok = Zen::WriteBufferToFile(doc.data(), value, size);
            if (!ok) {
                LogE("%s failed! path:%s", doc.data());
            }
            return ok;
        }

        std::vector<uint8_t> LoadURL(std::string const &url) override {

            return CallJavaBytesMethod(CLASS_NAME, "LoadURL", url);
        }

        std::string GetSystemLanguage() override {
            return CallJavaStringMethod(CLASS_NAME, "GetSystemLanguage");
        }

        static UtilsAndroid__ *Instance() {
            static auto me = new UtilsAndroid__;
            return me;
        }
    };

    UtilsAndroid *UtilsAndroid::Instance() {
        return UtilsAndroid__::Instance();
    }

    Utils *Utils::Instance() {
        return UtilsAndroid__::Instance();
    }
}

#endif
